﻿// -----------------------------------------------------------------------
// <copyright file="CadenceTaskManager.cs" company="The Usual Dosage">
// © 2013 by Matthew M. Martin
// </copyright>
// -----------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using Cadence.NET.Attributes;
using log4net;
using Cadence.NET.BaseClasses;
using System.Net;
using Cadence.NET.Models;
using Newtonsoft.Json;
using System.Reflection;
using log4net.Appender;
using System.IO;
using Cadence.NET.Examples;

namespace Cadence.NET
{
    /// <summary>
    /// Parent class which contains a collection of tasks, an interval definition, and manager methods to
    /// execute the tasks.
    /// </summary>
    public abstract class CadenceTaskManager
    {
        #region Private Members

        /// <summary>
        /// The error count
        /// </summary>
        private int errorCount = 0;

        /// <summary>
        /// Gets a reference to the Log4Net logger
        /// </summary>
        protected static readonly ILog log = LogManager.GetLogger(typeof(CadenceTaskManager));

        #endregion Private Members

        #region Properties

        /// <summary>
        /// Gets or sets the path the manager will serialize tasks to in case of a dump.
        /// </summary>
        /// <value>
        /// The task dump path.
        /// </value>
        public string TaskDumpPath 
        {
            get
            {
                string path = string.Format("{0}Data\\", System.Reflection.Assembly.GetExecutingAssembly().Location);
                string fileName = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name;
                path = path.Replace(fileName + ".dll", string.Empty);

                if (!string.IsNullOrWhiteSpace(ConfigurationManager.AppSettings["TaskDumpPath"]))
                {
                    path = ConfigurationManager.AppSettings["TaskDumpPath"];
                }
                return path;
            }
        }

        /// <summary>
        /// Gets the maximum error.
        /// </summary>
        /// <value>
        /// The maximum error.
        /// </value>
        private int MaxError
        {
            get
            {
                int maxError = 10;
                
                if (!string.IsNullOrWhiteSpace(ConfigurationManager.AppSettings["MaxErrorCount"]))
                {
                    int.TryParse(ConfigurationManager.AppSettings["MaxErrorCount"], out maxError);
                }
                return maxError;
            }
        }

        /// <summary>
        /// Gets or sets the task timer.
        /// </summary>
        /// <value>
        /// The task timer.
        /// </value>
        private Timer taskTimer { get; set; }

        /// <summary>
        /// Gets or sets the manager started.
        /// </summary>
        /// <value>
        /// The manager started.
        /// </value>
        private DateTime managerStarted { get; set; }

        /// <summary>
        /// Gets the log4net logger.
        /// </summary>
        public ILog Log
        {
            get
            {
                return log;
            }
        }

        /// <summary>
        /// Gets or sets the active tasks.
        /// </summary>
        /// <value>
        /// The active tasks.
        /// </value>
        private List<Task> ActiveTasks { get; set; }

        /// <summary>
        /// Gets or sets the tasks.
        /// </summary>
        /// <value>
        /// The tasks.
        /// </value>
        public List<Task> Tasks { get; set; }

        /// <summary>
        /// Gets or sets the interval.
        /// </summary>
        /// <value>
        /// The interval.
        /// </value>
        public Interval Interval { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether this instance has run.
        /// </summary>
        /// <value>
        ///   <c>true</c> if this instance has run; otherwise, <c>false</c>.
        /// </value>
        private bool RunOnce { get; set; }

        #endregion Properties

        #region Constructor

        /// <summary>
        /// Initializes a new instance of the <see cref="CadenceTaskManager"/> class.
        /// </summary>
        public CadenceTaskManager()
        {
            // Configure the logger
            if (ConfigurationManager.GetSection("log4net") != null)
                log4net.Config.XmlConfigurator.Configure();

            // If configured, fire up the internal webserver which can be used to queue requests and check queue status
            string webserverAddress = ConfigurationManager.AppSettings["WebserverListenerAddress"];
            if (!string.IsNullOrEmpty(webserverAddress))
            {
                log.Info("Starting Cadence.NET webserver...");
                Webserver ws = new Webserver(SendResponse, webserverAddress);
                ws.Start();
            }
        }

        #endregion Constructor

        #region Public Methods

        /// <summary>
        /// Starts this instance.
        /// </summary>
        public virtual void Start()
        {
            if (Interval == null)
            {
                throw new ArgumentNullException("Interval must be set for task manager to start");
            }

            if (Tasks == null)
            {
                throw new ArgumentNullException("No tasks found to process.");
            }

            // Wait N milliseconds before starting the task
            if (Interval.StartOffset != null)
            {
                Thread.Sleep(Interval.StartOffset.CalculateInterval());
            }

            // Start the task timer with the set interval options
            RunOnce = Interval.RunOptions == NET.Interval.IntervalRunOptions.RunOnce;

            // Configure the timer to run once
            taskTimer = new Timer(PerformTasks, null, Interval.Schedule.CalculateInterval(), Timeout.Infinite);

            // Set the start date and time
            managerStarted = DateTime.Now;
        }

        #endregion Public Methods

        #region Private Methods

        /// <summary>
        /// Saves all of the tasks to disk in JSON format.
        /// </summary>
        private void SaveTasksToDisk()
        {
            int taskIndex = 0;
            if (Tasks != null && Tasks.Count > 0)
            {
                Tasks.ForEach(t =>
                {
                    File.WriteAllText(string.Format("{0}\\Tasks\\task_{1}.tsk", TaskDumpPath, taskIndex++), JsonConvert.SerializeObject(t));
                });
            }

            if (ActiveTasks != null && ActiveTasks.Count > 0)
            {
                taskIndex = 0;
                ActiveTasks.ForEach(t =>
                {
                    File.WriteAllText(string.Format("{0}\\ActiveTasks\\task_{1}.tsk", TaskDumpPath, taskIndex++), JsonConvert.SerializeObject(t));
                });
            }
        }

        /// <summary>
        /// Sends a response from the internal webserver.
        /// </summary>
        /// <param name="request">The request.</param>
        /// <returns></returns>
        private string SendResponse(HttpListenerRequest request)
        {
            log.InfoFormat("Received HTTP {0} request from {1} for resource {2}.", request.HttpMethod, request.RemoteEndPoint.ToString(), request.RawUrl);

            string uri = ConfigurationManager.AppSettings["WebserverListenerAddress"];

            // Split the request into parts so we can process the methods
            string[] urlParts = request.RawUrl.Split('/');

            // Get the version to display
            Version version = Assembly.GetExecutingAssembly().GetName().Version;

            // Get the log file appender
            var fileAppender = LogManager.GetRepository().GetAppenders().First(appender => appender is RollingFileAppender);

            if (urlParts.Length <= 2)
            {
                // Push out the response
                CadenceStatusWebResponse response = new CadenceStatusWebResponse()
                {
                    Status = string.Format("Cadence.NET Task Manager v{0}.{1} is online and {2}. There are {3} tasks in the queue ({4} runnable). Currently logging to {5}. Available GET web methods are:",
                        version.Major,
                        version.Minor,
                        taskTimer != null ? "started" : "stopped",
                        Tasks != null ? Tasks.Count : 0,
                        Tasks != null ? Tasks.Where(t => t.IsRunnable).Count() : 0,
                        fileAppender == null ? "nowhere" : (fileAppender as RollingFileAppender).File),
                    StartDate = DateTime.Now,
                    Commands = new List<string>() { 
                    string.Format("{0}StopManager", uri),
                    string.Format("{0}StartManager", uri), 
                    string.Format("{0}FlushActiveTasks", uri),
                    string.Format("{0}FlushTasks", uri),
                    string.Format("{0}SerializeTasks", uri),
                    string.Format("{0}ResetTimer", uri),
                    string.Format("{0}AddTask/AssemblyName/TaskName", uri),
                    string.Format("{0}RemoveTask/AssemblyName/TaskName", uri)
                },
                    ActiveTasks = this.ActiveTasks,
                    AllTasks = this.Tasks,
                    Interval = this.Interval,
                    RunOnce = this.RunOnce
                };

                // Only show the logs if the user is authenticated or local
                if (request.IsAuthenticated || request.IsLocal)
                {
                    response.Commands.Add(string.Format("{0}ShowLogs", uri));
                }

                // Serialize the response to JSON and return to the client
                return JsonConvert.SerializeObject(response);
            }
            else
            {
                CadenceCommandWebResponse response = new CadenceCommandWebResponse() { };

                switch (urlParts[2].ToLower())
                {
                    default: // Default if unrecognized is 405 response
                        response.ResponseCode = 405;
                        response.ResponseMessage = "Method not allowed.";
                        break;
                    case "showlogs":
                        {
                            if (request.IsAuthenticated || request.IsLocal) // If the request is local or authenticated, dump the log
                            {
                                string filePath = (fileAppender as RollingFileAppender).File;

                                using (var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
                                {
                                    using (var textReader = new StreamReader(fileStream))
                                    {
                                        return textReader.ReadToEnd();
                                    }
                                }
                            }
                            else
                            {
                                response.ResponseCode = 401;
                                response.ResponseMessage = "Unauthorized.";
                            }
                            break;
                        }
                    case "stopmanager": // Stops the manager by nullifying the timer
                        try
                        {
                            lock (taskTimer)
                            {
                                taskTimer = null;
                            }
                            response.ResponseCode = 200;
                            response.ResponseMessage = "Ok.";
                        }
                        catch (Exception exc)
                        {
                            log.Error(exc);
                            response.ResponseCode = 400;
                            response.ResponseMessage = "Bad request. Check error logs.";
                        }
                        break;
                    case "serializetasks":
                        try
                        {
                            lock (Tasks)
                            {
                                lock (ActiveTasks)
                                {
                                    SaveTasksToDisk();
                                }
                            }
                            response.ResponseCode = 200;
                            response.ResponseMessage = "Ok.";
                        }
                        catch (Exception exc)
                        {
                            log.Error(exc);
                            response.ResponseCode = 400;
                            response.ResponseMessage = "Bad request. Check error logs.";
                        }
                        break;
                    case "startmanager": // Restart the manager
                        try
                        {
                            Start();
                            response.ResponseCode = 200;
                            response.ResponseMessage = "Ok.";
                        }
                        catch (Exception exc)
                        {
                            log.Error(exc);
                            response.ResponseCode = 400;
                            response.ResponseMessage = "Bad request. Check error logs.";
                        }
                        break;
                    case "flushactivetasks": // Nullifies all active tasks
                        try
                        {
                            lock (ActiveTasks)
                            {
                                ActiveTasks = null;
                            }
                            response.ResponseCode = 200;
                            response.ResponseMessage = "Ok.";
                        }
                        catch (Exception exc)
                        {
                            log.Error(exc);
                            response.ResponseCode = 400;
                            response.ResponseMessage = "Bad request. Check error logs.";
                        }
                        break;
                    case "flushtasks": // Nullifies all tasks for this manager
                        try
                        {
                            lock (Tasks)
                            {
                                Tasks = null;
                            }
                            response.ResponseCode = 200;
                            response.ResponseMessage = "Ok.";
                        }
                        catch (Exception exc)
                        {
                            log.Error(exc);
                            response.ResponseCode = 400;
                            response.ResponseMessage = "Bad request. Check error logs.";
                        }
                        break;
                    case "resettimer": // Resets the timer to the default setting
                        try
                        {
                            taskTimer.Change(Math.Max(0, Interval.Schedule.CalculateInterval()), Timeout.Infinite);
                            response.ResponseCode = 200;
                            response.ResponseMessage = "Ok.";
                        }
                        catch (Exception exc)
                        {
                            log.Error(exc);
                            response.ResponseCode = 400;
                            response.ResponseMessage = "Bad request. Check error logs.";
                        }
                        break;
                    case "addtask": // Adds a new task
                        try
                        {
                            if (urlParts.Length != 5) // Requires assembly name and type name
                            {
                                response.ResponseCode = 405;
                                response.ResponseMessage = "Method not allowed.";
                            }
                            else
                            {
                                AddTask(urlParts[3], urlParts[4]);
                                response.ResponseCode = 200;
                                response.ResponseMessage = "Ok.";
                            }
                        }
                        catch (Exception exc)
                        {
                            log.Error(exc);
                            response.ResponseCode = 400;
                            response.ResponseMessage = "Bad request. Check error logs.";
                        }
                        break;
                    case "removetask": // Removes a task
                        try
                        {
                            if (urlParts.Length != 5) // Requires assembly name and type name
                            {
                                response.ResponseCode = 405;
                                response.ResponseMessage = "Method not allowed.";
                            }
                            else
                            {
                                RemoveTask(urlParts[4], urlParts[5]);
                                response.ResponseCode = 200;
                                response.ResponseMessage = "Ok.";
                            }
                        }
                        catch (Exception exc)
                        {
                            log.Error(exc);
                            response.ResponseCode = 400;
                            response.ResponseMessage = "Bad request. Check error logs.";
                        }
                        break;

                }

                // Serialize the response to JSON and return to the client
                return JsonConvert.SerializeObject(response);
            }
        }

        /// <summary>
        /// Performs the task asynchronously.
        /// </summary>
        /// <param name="obj">The obj.</param>
        private void PerformTaskAsync(object obj)
        {
            Task t = (Task)obj;
            t.PerformTask(t.TaskContext);
            RemoveActiveTask(t);
        }

        /// <summary>
        /// Adds the active task.
        /// </summary>
        /// <param name="t">The t.</param>
        private void AddActiveTask(Task t)
        {
            if (ActiveTasks == null)
                ActiveTasks = new List<Task>();

            lock (ActiveTasks)
            {
                // Add to the tasks we are tracking
                ActiveTasks.Add(t);
            }
        }

        /// <summary>
        /// Adds the task.
        /// </summary>
        /// <param name="assemblyName">Name of the assembly.</param>
        /// <param name="taskName">Name of the task.</param>
        private void AddTask(string assemblyName, string taskName)
        {
            if (Tasks == null)
                Tasks = new List<Task>();

            lock (Tasks)
            {
                object task = Activator.CreateInstance(assemblyName, taskName);

                // Add to the tasks we are tracking
                Tasks.Add(task as Task);
            }
        }

        /// <summary>
        /// Removes the active task.
        /// </summary>
        /// <param name="t">The t.</param>
        private void RemoveActiveTask(Task t)
        {
            if (ActiveTasks == null)
                return;

            var task = ActiveTasks.Where(a => (a.GetType() == t.GetType())).FirstOrDefault();
            if (task != null)
            {
                lock (ActiveTasks)
                {
                    ActiveTasks.Remove(task);
                }
            }
        }

        /// <summary>
        /// Removes the active task (called by web method).
        /// </summary>
        /// <param name="taskName">Name of the task.</param>
        private void RemoveTask(string assemblyName, string taskName)
        {
            if (Tasks == null)
                return;

            var task = Tasks.Where(a => (a.GetType().Assembly.FullName.ToLower() == assemblyName.ToLower() && a.GetType().Name.ToLower() == taskName.ToLower())).FirstOrDefault();
            if (task != null)
            {
                lock (Tasks)
                {
                    Tasks.Remove(task);
                }
            }
        }

        /// <summary>
        /// Performs the tasks.
        /// </summary>
        /// <param name="state">The state.</param>
        private void PerformTasks(object state)
        {
            try
            {
                if (Tasks != null)
                {
                    // Time how long the execution takes
                    Stopwatch watch = new Stopwatch();
                    watch.Start();

                    Tasks.ForEach(t =>
                    {
                        // Execute each runnable task
                        if (t.IsRunnable)
                        {
                            AddActiveTask(t);

                            // See if this task has any behavior attributes defined
                            object attrib = t.GetType().GetCustomAttributes(true).Where(a => (a is TaskBehavior)).FirstOrDefault();
                            if (attrib != null)
                            {
                                TaskBehavior tb = (TaskBehavior)attrib;
                                // Make sure we're not already running a task of this type
                                bool hasMultipleTasks = ActiveTasks.Where(a => (a.GetType() == t.GetType())).Count() == 0;
                                if (!tb.RunConcurrent && !hasMultipleTasks)
                                {
                                    if (tb.RunAsynch) // Run the task on its own thread
                                    {
                                        // Create a thread and fire it off
                                        var thread = new Thread(
                                            () => PerformTaskAsync(t));
                                        thread.Start();
                                    }
                                    else // Run synchronously
                                    {
                                        t.PerformTask(t.TaskContext);
                                    }
                                }
                                else if (tb.RunConcurrent)
                                {
                                    if (tb.RunAsynch) // Run the task on its own thread
                                    {
                                        AddActiveTask(t);

                                        // Create a thread and fire it off
                                        var thread = new Thread(
                                            () => PerformTaskAsync(t));
                                        thread.Start();
                                    }
                                    else // Run synchronously
                                    {
                                        t.PerformTask(t.TaskContext);
                                    }
                                }
                            }
                            else // No attributes defined, run normally
                            {
                                t.PerformTask(t.TaskContext);
                            }
                            RemoveActiveTask(t);
                        }
                    });

                    watch.Stop();

                    if (RunOnce) // Interval set to just run once, so kill the timer
                    {
                        taskTimer.Change(Timeout.Infinite, Timeout.Infinite);
                        taskTimer = null;
                    }
                    else if (Interval.Precision == NET.Interval.IntervalPrecision.Exact) // Offset the next run by the amount of time it took for the task to execute
                    {
                        if (watch != null)
                        {
                            // Adjust the interval to account for the execution time
                            taskTimer.Change(Math.Max(0, Interval.Schedule.CalculateInterval() - watch.ElapsedMilliseconds), Timeout.Infinite);
                        }
                        else // Watch was null for some reason, so just reset the timer to fire again at the next interval
                        {
                            taskTimer.Change(Math.Max(0, Interval.Schedule.CalculateInterval()), Timeout.Infinite);
                        }
                    }
                    else // Reset the timer to fire again at the next interval
                    {
                        taskTimer.Change(Math.Max(0, Interval.Schedule.CalculateInterval()), Timeout.Infinite);
                    }
                }
            }
            catch (Exception exc)
            {
                // Log the unhandled exception. If we exceed our threshold, shut down the manager.
                log.Error(exc);
                errorCount++;
                if (errorCount >= MaxError)
                {
                    taskTimer = null;
                    log.ErrorFormat("Cadence.NET logged {0} unhandled exceptions which was over the error threshold of {1}. Stopping the manager & serializing all tasks to disk.", errorCount, MaxError);
                    try
                    {
                        SaveTasksToDisk();
                    }
                    catch (Exception serxc) 
                    {
                        log.Error(serxc);
                    }
                }
            }
        }

        #endregion Private Methods
    }
}
